package repository

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"mall/common/globalkey"
	"mall/common/modelx"
	"mall/service/backend/model"

	"github.com/sirupsen/logrus"
	"github.com/volatiletech/null/v8"
	"github.com/volatiletech/sqlboiler/v4/boil"
	"github.com/volatiletech/sqlboiler/v4/queries/qm"
	"github.com/zeromicro/go-zero/core/stores/cache"
)

type (
	// SysMenuRepository is an interface to be customized, add more methods here,
	// and implement the added methods in customSysMenuRepository.
	SysMenuRepository interface {
		FindOne(ctx context.Context, id uint64) (*model.SysMenu, error)
		FindOneByName(ctx context.Context, name string) (*model.SysMenu, error)
		FindOneWithExpire(
			ctx context.Context,
			id uint64,
			expire time.Duration,
		) (*model.SysMenu, error)
		FindOneNoCache(
			ctx context.Context,
			conn boil.ContextExecutor,
			id uint64,
		) (*model.SysMenu, error)
		GetList(
			ctx context.Context,
			cols model.M,
			withCount int,
			filter map[string]map[string][]any,
		) ([]*model.SysMenu, int64, error)
		Insert(ctx context.Context, tx boil.ContextExecutor, data *model.SysMenu) error
		Update(ctx context.Context, tx boil.ContextExecutor, data *model.SysMenu) error
		UpdateBatch(ctx context.Context, tx boil.ContextExecutor, ids []uint64, cols model.M) error
		SoftDelete(ctx context.Context, tx boil.ContextExecutor, id uint64) error
		Restore(ctx context.Context, tx boil.ContextExecutor, id uint64) error
		ForceDelete(ctx context.Context, tx boil.ContextExecutor, id uint64) error
		ForceDeleteBatch(ctx context.Context, tx boil.ContextExecutor, ids []uint64) error
	}

	customSysMenuRepository struct {
		modelx.CachedConn
		mysqlConn           *sql.DB
		cacheIdPrefix       string
		cacheNamePrefix     string
		cacheIdExpirePrefix string
	}
)

// NewSysMenuRepository returns a repository for the database table.
func NewSysMenuRepository(mysqlConn *sql.DB, cacheConf cache.CacheConf) SysMenuRepository { /////
	return &customSysMenuRepository{
		CachedConn:          modelx.NewConn(mysqlConn, cacheConf),
		mysqlConn:           mysqlConn,
		cacheIdPrefix:       "cache:backend:sys_menus:id:",
		cacheNamePrefix:     "cache:backend:sys_menus:name:",
		cacheIdExpirePrefix: "cache:backend:sys_menus:id:expire:",
	}
}

func (r *customSysMenuRepository) FindOne(
	ctx context.Context,
	id uint64,
) (*model.SysMenu, error) {
	cacheIdKey := r.formatPrimary(id)
	var resp model.SysMenu
	err := r.QueryCtx(ctx, &resp, cacheIdKey, func(ctx context.Context, conn *sql.DB, v any) error {
		data, err := model.FindSysMenu(ctx, conn, id)
		if err != nil {
			return err
		}

		*v.(*model.SysMenu) = *data

		return nil
	})

	switch err {
	case nil:
		return &resp, nil
	case sql.ErrNoRows:
		return nil, model.ErrNotFound
	default:
		return nil, err
	}
}

func (r *customSysMenuRepository) FindOneByName(
	ctx context.Context,
	name string,
) (*model.SysMenu, error) {
	cacheNameKey := r.formatNameKey(name)
	var resp model.SysMenu
	err := r.QueryRowIndexCtx(ctx, &resp, cacheNameKey, r.formatPrimary,
		func(ctx context.Context, conn *sql.DB, v any) (i any, e error) {
			mods := []qm.QueryMod{
				model.SysMenuWhere.Name.EQ(name),
			}
			list, err := model.SysMenus(mods...).All(ctx, conn)
			if err != nil {
				return nil, err
			}

			*v.(*model.SysMenu) = *list[0]

			return list[0].ID, nil
		}, r.queryPrimary)

	switch err {
	case nil:
		return &resp, nil
	case sql.ErrNoRows:
		return nil, model.ErrNotFound
	default:
		return nil, err
	}
}

func (r *customSysMenuRepository) FindOneWithExpire(
	ctx context.Context,
	id uint64,
	expire time.Duration,
) (*model.SysMenu, error) {
	cacheIdKey := r.formatPrimary(id)
	var resp model.SysMenu
	err := r.QueryWithExpireCtx(ctx, &resp, cacheIdKey, expire, func(ctx context.Context, conn *sql.DB, v any) error {
		data, err := model.FindSysMenu(ctx, conn, id)
		if err != nil {
			return err
		}

		*v.(*model.SysMenu) = *data

		return nil
	})

	switch err {
	case nil:
		return &resp, nil
	case sql.ErrNoRows:
		return nil, model.ErrNotFound
	default:
		return nil, err
	}
}

func (r *customSysMenuRepository) FindOneNoCache(
	ctx context.Context,
	conn boil.ContextExecutor,
	id uint64,
) (*model.SysMenu, error) {
	var db boil.ContextExecutor = r.mysqlConn
	if conn != nil {
		db = conn
	}
	data, err := model.FindSysMenu(ctx, db, id)
	logrus.Info(fmt.Sprintf("FindOneNoCache err: %+v", err))
	switch err {
	case nil:
		return data, nil
	case sql.ErrNoRows:
		return nil, model.ErrNotFound
	default:
		return nil, err
	}
}

func (r *customSysMenuRepository) GetList(
	ctx context.Context,
	cols model.M,
	withCount int, // 0-返回count；1-返回count和list；2-根据offset返回count和list；3-根据preMinId返回count和list；4-根据preMaxId返回count和list
	filter map[string]map[string][]any,
) (list []*model.SysMenu, count int64, err error) {
	var page int
	var pageSize int
	var limit int
	var selectCol string
	var mods []qm.QueryMod
	var nextCount int64
	var previousCount int64
	for k, v := range cols {
		switch k {
		case "page":
			if val, ok := v.(uint64); ok && val > 1 {
				page = int(val)
			}
		case "pageSize":
			if val, ok := v.(uint64); ok && val > 0 {
				pageSize = int(val)
			}
		case "limit":
			if val, ok := v.(uint32); ok && val > 0 {
				limit = int(val)
			}
		case "offset":
			if withCount == 2 {
				if val, ok := v.(uint32); ok && val > 0 {
					mods = append(mods, qm.Offset(int(val)))
					previousCount = 1
				}
			}
		case "preMinId":
			if withCount == 3 {
				if val, ok := v.(uint64); ok && val > 0 {
					mods = append(mods, qm.OrderBy(model.SysMenuColumns.ID + " DESC"))
					mods = append(mods, model.SysMenuWhere.ID.LT(val))
					previousCount = 1
				}
			}
		case "preMaxId":
			if withCount == 4 {
				if val, ok := v.(uint64); ok && val > 0 {
					mods = append(mods, qm.OrderBy(model.SysMenuColumns.ID + " ASC"))
					mods = append(mods, model.SysMenuWhere.ID.GT(val))
					previousCount = 1
				}
			}
		case "orderBy":
			if withCount != 3 && withCount != 4 {
				if val, ok := v.(string); ok && val != "" {
					mods = append(mods, qm.OrderBy(val))
				}
			}
		case "select":
			if val, ok := v.(string); ok && val != "" {
				selectCol = val
			}
		}
	}

	if filter != nil {
		for key, opMap := range filter {
			switch key {
			case model.SysMenuColumns.ID:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.ID, opMap)...)
			case model.SysMenuColumns.ParentID:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.ParentID, opMap)...)
			case model.SysMenuColumns.MenuType:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.MenuType, opMap)...)
			case model.SysMenuColumns.MenuLevel:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.MenuLevel, opMap)...)
			case model.SysMenuColumns.Sort:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.Sort, opMap)...)
			case model.SysMenuColumns.Path:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.Path, opMap)...)
			case model.SysMenuColumns.RealPath:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.RealPath, opMap)...)
			case model.SysMenuColumns.Name:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.Name, opMap)...)
			case model.SysMenuColumns.Title:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.Title, opMap)...)
			case model.SysMenuColumns.Icon:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.Icon, opMap)...)
			case model.SysMenuColumns.Redirect:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.Redirect, opMap)...)
			case model.SysMenuColumns.Component:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.Component, opMap)...)
			case model.SysMenuColumns.HideMenu:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.HideMenu, opMap)...)
			case model.SysMenuColumns.HideBreadcrumb:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.HideBreadcrumb, opMap)...)
			case model.SysMenuColumns.HideTab:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.HideTab, opMap)...)
			case model.SysMenuColumns.HideChildrenInMenu:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.HideChildrenInMenu, opMap)...)
			case model.SysMenuColumns.IgnoreKeepAlive:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.IgnoreKeepAlive, opMap)...)
			case model.SysMenuColumns.Affix:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.Affix, opMap)...)
			case model.SysMenuColumns.FrameSRC:
				mods = append(mods, modelx.GetStringMods(model.SysMenuColumns.FrameSRC, opMap)...)
			case model.SysMenuColumns.CarryParam:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.CarryParam, opMap)...)
			case model.SysMenuColumns.DynamicLevel:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.DynamicLevel, opMap)...)
			case model.SysMenuColumns.Status:
				mods = append(mods, modelx.GetIntMods(model.SysMenuColumns.Status, opMap)...)
			}
		}
	}

	if withCount >= 2 {
		if limit > 0 {
			mods = append(mods, qm.Limit(limit + 1))
		}
	} else if withCount >= 0 {
		count, err = model.SysMenus(mods...).Count(ctx, r.mysqlConn)
		if err != nil {
			return
		}

		if withCount == 0 {
			return
		}

		if pageSize > 0 {
			if page > 1 {
				mods = append(mods, qm.Offset(pageSize*(page-1)))
			}
			mods = append(mods, qm.Limit(pageSize))
		}
	} else {
		if pageSize > 0 {
			if page > 1 {
				mods = append(mods, qm.Offset(pageSize*(page-1)))
			}
			mods = append(mods, qm.Limit(pageSize))
		}
	}

	var columns []string
	if selectCol != "" {
		if selectCol != "," {
			columns = strings.Split(selectCol, ",")
		} else {
			columns = append(columns, model.SysMenuColumns.ID)
		}
	}
	if len(columns) > 0 {
		mods = append(mods, qm.Select(columns...))
	}

	list, err = model.SysMenus(mods...).All(ctx, r.mysqlConn)
	if withCount >= 2 {
		if limit > 0 && len(list) > limit {
			nextCount = 1
			list = list[:limit]
		}
		count = previousCount + nextCount * 2
	}

	return
}

func (r *customSysMenuRepository) Insert(
	ctx context.Context,
	tx boil.ContextExecutor,
	data *model.SysMenu,
) error {
	err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		return data.Insert(ctx, db, boil.Infer())
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customSysMenuRepository) Update(
	ctx context.Context,
	tx boil.ContextExecutor,
	data *model.SysMenu,
) error {
	err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		rowsAff, err := data.Update(ctx, db, boil.Blacklist(
			model.SysMenuColumns.ID,
			model.SysMenuColumns.DeletedAt,
		))
		if err != nil {
			return err
		}

		if rowsAff == 0 {
			return model.ErrNotUpdated
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customSysMenuRepository) UpdateBatch(
	ctx context.Context,
	tx boil.ContextExecutor,
	ids []uint64,
	cols model.M,
) error {
	if len(ids) > 0 {
		err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
			var db boil.ContextExecutor
			if tx != nil {
				db = tx
			} else {
				db = conn
			}

			var mods []qm.QueryMod
			mods = append(mods, model.SysMenuWhere.ID.IN(ids))

			rowsAff, err := model.SysMenus(mods...).UpdateAll(ctx, db, cols)
			if err != nil {
				return err
			}

			if rowsAff == 0 {
				return model.ErrNotUpdated
			}

			return nil
		}, r.formatPrimaries(ids)...)

		return err
	}

	return nil
}

func (r *customSysMenuRepository) SoftDelete(
	ctx context.Context,
	tx boil.ContextExecutor,
	id uint64,
) error {
	data, err := r.FindOneNoCache(ctx, tx, id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil
		}

		return err
	}

	if data.Status == uint8(globalkey.StatusDeleted) {
		return nil
	}

	data.DelStatus = data.Status
	data.Status = uint8(globalkey.StatusDeleted)
	data.DeletedAt = null.TimeFrom(time.Now())

	err = r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		rowsAff, err := data.Update(ctx, db, boil.Blacklist(
			model.SysMenuColumns.ID,
			model.SysMenuColumns.UpdatedAt,
		))
		if err != nil {
			return err
		}

		if rowsAff == 0 {
			return model.ErrNotUpdated
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customSysMenuRepository) Restore(
	ctx context.Context,
	tx boil.ContextExecutor,
	id uint64,
) error {
	data, err := r.FindOneNoCache(ctx, tx, id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil
		}

		return err
	}

	if data.Status != uint8(globalkey.StatusDeleted) {
		return nil
	}

	data.Status = data.DelStatus
	data.DeletedAt = null.TimeFromPtr(nil)

	err = r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		rowsAff, err := data.Update(ctx, db, boil.Blacklist(
			model.SysMenuColumns.ID,
			model.SysMenuColumns.UpdatedAt,
		))
		if err != nil {
			return err
		}

		if rowsAff == 0 {
			return model.ErrNotUpdated
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customSysMenuRepository) ForceDelete(
	ctx context.Context,
	tx boil.ContextExecutor,
	id uint64,
) error {
	data, err := r.FindOneNoCache(ctx, tx, id)
	if err != nil {
		if err == model.ErrNotFound {
			return nil
		}

		return err
	}

	err = r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
		var db boil.ContextExecutor
		if tx != nil {
			db = tx
		} else {
			db = conn
		}

		rowsAff, err := data.Delete(ctx, db)
		if err != nil {
			return err
		}

		if rowsAff == 0 {
			return model.ErrNotDeleted
		}

		return nil
	}, r.getCacheKeys(data)...)

	return err
}

func (r *customSysMenuRepository) ForceDeleteBatch(
	ctx context.Context,
	tx boil.ContextExecutor,
	ids []uint64,
) error {
	if len(ids) > 0 {
		err := r.ExecCtx(ctx, func(ctx context.Context, conn *sql.DB) error {
			var db boil.ContextExecutor
			if tx != nil {
				db = tx
			} else {
				db = conn
			}

			var mods []qm.QueryMod
			mods = append(mods, model.SysMenuWhere.ID.IN(ids))

			rowsAff, err := model.SysMenus(mods...).DeleteAll(ctx, db)
			if err != nil {
				return err
			}

			if rowsAff == 0 {
				return model.ErrNotDeleted
			}

			return nil
		}, r.formatPrimaries(ids)...)

		return err
	}

	return nil
}

func (r *customSysMenuRepository) getCacheKeys(data *model.SysMenu) []string {
	if data == nil {
		return []string{}
	}

	cacheIdKey := r.formatPrimary(data.ID)
	cacheNameKey := r.formatNameKey(data.Name)
	cacheKeys := []string{
		cacheIdKey,
		cacheNameKey,
	}

	return cacheKeys
}

func (r *customSysMenuRepository) formatPrimary(primary any) string {
	return fmt.Sprintf("%s%v", r.cacheIdPrefix, primary)
}

func (r *customSysMenuRepository) formatPrimaries(primaries []uint64) []string {
	var resp []string
	for _, primary := range primaries {
		resp = append(resp, r.formatPrimary(primary))
	}

	return resp
}

func (r *customSysMenuRepository) formatNameKey(name string) string {
	return fmt.Sprintf("%s%s", r.cacheNamePrefix, name)
}

func (r *customSysMenuRepository) queryPrimary(ctx context.Context, conn *sql.DB, v, primary any) error {
	var uid uint64
	if jsonId, ok := primary.(json.Number); ok {
		if int64Id, err := jsonId.Int64(); err == nil {
			uid = uint64(int64Id)
		} else {
			return err
		}
	}

	data, err := model.FindSysMenu(ctx, conn, uid)
	if err != nil {
		return err
	}

	*v.(*model.SysMenu) = *data

	return nil
}
